/*
 * jxUtils Project (2013, 2014)
 * 
 * by Denilson Edinaldo Pinto
 * 
 * LGPL
 * Copyright (C) 2014
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.jxUtils.date;

import java.sql.Time;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

import org.jxUtils.check.ParameterCheck;
import org.jxUtils.date.BrazilianCalendar.HolydayDate;
import org.jxUtils.i18n.I18N;
import org.jxUtils.system.SystemProperties;

/**
 * @author Denilson Edinaldo Pinto
 * @project jxUtils
 * @package org.jxUtils.date
 * @date 28/05/2013 - 21:34:37
 */
public final class DateUtil {
	
	private static final HourFormatters HOUR_FORMATTERS = new HourFormatters();
	
	private static final DateFormatters DATE_FORMATTERS = new DateFormatters();
	
	private static final DateHourFormatters DATE_HOUR_FORMATTERS = new DateHourFormatters();
	
	private static final SimpleDateFormat FORMAT = new SimpleDateFormat();
	
	private static final SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
	
	/**
	 * Obtém a quantidade de dias entre o <b>startDate</b> e o <b>endDate</b>
	 * 
	 * @param startDate
	 * @param endDate
	 * @return days
	 */
	public static int countDays(final Date startDate, final Date endDate) {
	
		ParameterCheck.isNull(startDate, "startDate");
		ParameterCheck.isNull(endDate, "endDate");
		
		// Obtém a quantidade de dias entre a data inicial e a data final
		final int days = (int)((endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000));
		
		return days > 0 ? days : 0;
	}
	
	/**
	 * @param endDate
	 * @return Working days
	 */
	public static int countWorkingDays(final Calendar endDate) {
	
		ParameterCheck.isNull(endDate, "endDate");
		
		final Calendar calendar = Calendar.getInstance();
		
		return countWorkingDays(calendar, calendar.getTime(), endDate.getTime());
	}
	
	/**
	 * @param calendar
	 * @param startDate
	 * @param endDate
	 * @return Working days in the calendar
	 */
	public static int countWorkingDays(final Calendar calendar, final Date startDate, final Date endDate) {
	
		ParameterCheck.isNull(calendar, "calendar");
		ParameterCheck.isNull(startDate, "startDate");
		ParameterCheck.isNull(endDate, "endDate");
		
		boolean isBrazilianCalendar = calendar instanceof BrazilianCalendar;
		
		int workingDays = 0;
		
		final int totalDays = countDays(startDate, endDate);
		
		// Setando o calendar com a Data Inicial
		calendar.setTime(startDate);
		
		//
		for(int i = 0; i <= totalDays; i++) {
			
			// Se não for fim de semana
			if( !(calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) && !(calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY)) {
				
				//
				if(isBrazilianCalendar) {
					
					final HolydayDate holydayDate = ((BrazilianCalendar)calendar).new HolydayDate(calendar.get(Calendar.DAY_OF_MONTH), calendar.get(Calendar.MONTH));
					
					// Se não for feriado
					if( !((BrazilianCalendar)calendar).getHolydayDates().values().contains(holydayDate)) {
						
						workingDays++;
					}
					
				}else {
					
					workingDays++;
				}
			}
			
			calendar.add(Calendar.DATE, 1);
			
		}
		
		return workingDays;
	}
	
	/**
	 * @param endDate
	 * @return Working days
	 */
	public static int countWorkingDays(final Date endDate) {
	
		ParameterCheck.isNull(endDate, "endDate");
		
		final Calendar calendar = Calendar.getInstance();
		
		return countWorkingDays(calendar, calendar.getTime(), endDate);
	}
	
	/**
	 * @param time
	 * @param currentUnit
	 * @param parseUnit
	 * @return Formated time to parseUnit
	 */
	public static String formatTime(final long time, final TimeUnit currentUnit, final TimeUnit parseUnit) {
	
		ParameterCheck.isNull(currentUnit, "currentUnit");
		ParameterCheck.isNull(parseUnit, "parseUnit");
		
		//
		if(parseUnit.equals(TimeUnit.NANOSECONDS)) {
			
			return currentUnit.toNanos(time) + " " + I18N.time().nanoseconds();
		}
		
		//
		if(parseUnit.equals(TimeUnit.MICROSECONDS)) {
			
			return currentUnit.toMicros(time) + " " + I18N.time().microseconds();
		}
		
		//
		if(parseUnit.equals(TimeUnit.MILLISECONDS)) {
			
			return currentUnit.toMillis(time) + " " + I18N.time().miliseconds();
		}
		
		//
		if(parseUnit.equals(TimeUnit.SECONDS)) {
			
			return currentUnit.toSeconds(time) + " " + I18N.time().seconds();
		}
		
		//
		if(parseUnit.equals(TimeUnit.MINUTES)) {
			
			return currentUnit.toMinutes(time) + " " + I18N.time().minutes();
		}
		
		//
		if(parseUnit.equals(TimeUnit.HOURS)) {
			
			return currentUnit.toHours(time) + " " + I18N.time().hours();
		}
		
		//
		if(parseUnit.equals(TimeUnit.DAYS)) {
			
			return currentUnit.toDays(time) + " " + I18N.time().days();
		}
		
		return "" + time;
	}
	
	/**
	 * @param gregorianCalendar
	 * @return AshWednesDay
	 */
	public static GregorianCalendar getAshWednesDay(final GregorianCalendar gregorianCalendar) {
	
		ParameterCheck.isNull(gregorianCalendar, "gregorianCalendar");
		
		final GregorianCalendar calendar = getCarnivalDay(gregorianCalendar);
		
		calendar.add(Calendar.DAY_OF_YEAR, 1);
		
		return calendar;
	}
	
	/**
	 * @param gregorianCalendar
	 * @return CarnivalDay
	 */
	public static GregorianCalendar getCarnivalDay(final GregorianCalendar gregorianCalendar) {
	
		ParameterCheck.isNull(gregorianCalendar, "gregorianCalendar");
		
		final GregorianCalendar calendar = getEasterDay(gregorianCalendar);
		
		calendar.roll(Calendar.DAY_OF_YEAR, -47);
		
		return calendar;
	}
	
	/**
	 * @param gregorianCalendar
	 * @return CorpusChristi
	 */
	public static GregorianCalendar getCorpusChristi(final GregorianCalendar gregorianCalendar) {
	
		ParameterCheck.isNull(gregorianCalendar, "gregorianCalendar");
		
		final GregorianCalendar calendar = getTrinitySunday(gregorianCalendar);
		
		calendar.add(Calendar.DAY_OF_YEAR, 4);
		
		return calendar;
	}
	
	/**
	 * @param gregorianCalendar
	 * @return EasterDay
	 */
	public static GregorianCalendar getEasterDay(final GregorianCalendar gregorianCalendar) {
	
		ParameterCheck.isNull(gregorianCalendar, "gregorianCalendar");
		
		final int year = gregorianCalendar.get(Calendar.YEAR);
		
		int a, b, c, d, e, f, g, h, i, j, k;
		int easterMonth;
		int easterDay;
		
		GregorianCalendar calendar = null;
		
		a = year % 19;
		b = year / 100;
		c = year % 100;
		d = b / 4;
		e = b % 4;
		f = (8 * b + 13) / 25;
		g = (19 * a + b - d - f + 15) % 30;
		h = c / 4;
		i = c % 4;
		j = (a + 11 * g) / 319;
		k = (2 * e + 2 * h - i - g + j + 32) % 7;
		
		easterMonth = (g - j + k + 90) / 25;
		easterDay = (g - j + k + easterMonth + 19) % 32;
		
		calendar = new GregorianCalendar(year, easterMonth - 1, easterDay);
		
		return calendar;
	}
	
	/**
	 * @param gregorianCalendar
	 * @return GoodFriDay
	 */
	public static GregorianCalendar getGoodFriDay(final GregorianCalendar gregorianCalendar) {
	
		ParameterCheck.isNull(gregorianCalendar, "gregorianCalendar");
		
		final GregorianCalendar calendar = getEasterDay(gregorianCalendar);
		
		calendar.roll(Calendar.DAY_OF_YEAR, -2);
		
		return calendar;
	}
	
	/**
	 * @param index
	 * @return the parser
	 */
	public static String getParserDate(final int index) {
	
		//
		if(index < 0 || index >= DATE_FORMATTERS.getPatterns().size()) {
			
			throw new IndexOutOfBoundsException(I18N.parameter().invalidRangeIndex(0, DATE_FORMATTERS.getPatterns().size() - 1));
		}
		
		return DATE_FORMATTERS.getPatterns().get(index);
	}
	
	/**
	 * @param index
	 * @return the parser
	 */
	public static String getParserDateHour(final int index) {
	
		//
		if(index < 0 || index >= DATE_HOUR_FORMATTERS.getPatterns().size()) {
			
			throw new IndexOutOfBoundsException(I18N.parameter().invalidRangeIndex(0, DATE_HOUR_FORMATTERS.getPatterns().size() - 1));
		}
		
		return DATE_HOUR_FORMATTERS.getPatterns().get(index);
	}
	
	/**
	 * @param index
	 * @return the parser
	 */
	public static String getParserHour(final int index) {
	
		//
		if(index < 0 || index >= HOUR_FORMATTERS.getPatterns().size()) {
			
			throw new IndexOutOfBoundsException(I18N.parameter().invalidRangeIndex(0, HOUR_FORMATTERS.getPatterns().size() - 1));
		}
		
		return HOUR_FORMATTERS.getPatterns().get(index);
	}
	
	/**
	 * @param gregorianCalendar
	 * @return PentecostDay
	 */
	public static GregorianCalendar getPentecostDay(final GregorianCalendar gregorianCalendar) {
	
		ParameterCheck.isNull(gregorianCalendar, "gregorianCalendar");
		
		final GregorianCalendar calendar = getEasterDay(gregorianCalendar);
		
		calendar.add(Calendar.DAY_OF_YEAR, 49);
		
		return calendar;
	}
	
	/**
	 * @param gregorianCalendar
	 * @return TrinitySunday
	 */
	public static GregorianCalendar getTrinitySunday(final GregorianCalendar gregorianCalendar) {
	
		ParameterCheck.isNull(gregorianCalendar, "gregorianCalendar");
		
		final GregorianCalendar calendar = getPentecostDay(gregorianCalendar);
		
		calendar.add(Calendar.DAY_OF_YEAR, 7);
		
		return calendar;
	}
	
	/**
	 * @return date formatted for location
	 */
	public static String now() {
	
		return DateFormat.getDateInstance().format(new Date());
	}
	
	/**
	 * @param format
	 * @return formatted date
	 */
	public static String now(final String format) {
	
		ParameterCheck.isNullOrAbsoluteEmpty(format, "format");
		
		return parse(new Date(), format);
	}
	
	/**
	 * @return date in format <b>dd/MM/yyyy</b>
	 */
	public static String now2() {
	
		return now("dd/MM/yyyy");
	}
	
	/**
	 * @return date in format <b>yyyy-MM-dd</b>
	 */
	public static String now3() {
	
		return now("yyyy-MM-dd");
	}
	
	/**
	 * @return date in format <b>dd/MM/yyyy HH:mm:ss</b>
	 */
	public static String now4() {
	
		return now("dd/MM/yyyy HH:mm:ss");
	}
	
	/**
	 * @return date in format <b>yyyy-MM-dd HH:mm:ss</b>
	 */
	public static String now5() {
	
		return now("yyyy-MM-dd HH:mm:ss");
	}
	
	/**
	 * @return {@link java.sql.Date}
	 */
	@SuppressWarnings("deprecation")
	public static java.sql.Date nowDate() {
	
		Date now = nowDateTime();
		
		return new java.sql.Date(now.getYear(), now.getMonth(), now.getDate());
	}
	
	/**
	 * @return {@link Date}
	 */
	public static Date nowDateTime() {
	
		return new Date();
	}
	
	/**
	 * @return {@link Time}
	 */
	@SuppressWarnings("deprecation")
	public static Time nowTime() {
	
		Date now = nowDateTime();
		
		return new Time(now.getHours(), now.getMinutes(), now.getSeconds());
	}
	
	/**
	 * @return {@link Timestamp}
	 */
	public static Timestamp nowTimestamp() {
	
		Date now = nowDateTime();
		
		return new Timestamp(now.getTime());
	}
	
	/**
	 * @return date in format <b>yyyy-MM-ddTHH:mm:ssXXXX</b>
	 */
	public static String nowUTC() {
	
		return parseToUTC(new Date());
	}
	
	/**
	 * @param date
	 * @return date formatted for location
	 */
	public static String parse(final Date date) {
	
		ParameterCheck.isNull(date, "date");
		
		return FORMAT.format(date);
	}
	
	/**
	 * @param date
	 * @param format
	 * @return formatted date
	 */
	public static String parse(final Date date, final String format) {
	
		ParameterCheck.isNull(date, "date");
		ParameterCheck.isNullOrAbsoluteEmpty(format, "format");
		
		return new SimpleDateFormat(format).format(date);
	}
	
	/**
	 * @param milliseconds
	 * @return date formatted for location
	 */
	public static String parse(final long milliseconds) {
	
		return parse(new Date(milliseconds));
	}
	
	/**
	 * @param value
	 * @param pattern
	 * @return the {@link Date}
	 * @throws ParseException
	 */
	public static Date parseToDate(final String value, final String pattern) throws ParseException {
		
		return new SimpleDateFormat(pattern).parse(value);
	}
	
	/**
	 * @param value
	 * @param tryHour
	 * @param tryDate
	 * @param tryDateHour
	 * @return the {@link Date}
	 * @throws ParseException
	 */
	public static Date parse(final String value, final boolean tryHour, final boolean tryDate, final boolean tryDateHour) throws ParseException {
		
		ParameterCheck.isNullOrAbsoluteEmpty(value, "value");
		
		// 
		if(tryHour) {
			
			//
			for(final String pattern : HOUR_FORMATTERS.getPatterns()) {
				
				try {
					return parseToDate(value, pattern);
					
				}catch(final ParseException parseException) {
					// ignore
				}
			}
		}
		
		// 
		if(tryDate) {
			
			//
			for(final String pattern : DATE_FORMATTERS.getPatterns()) {
				
				try {
					return parseToDate(value, pattern);
					
				}catch(final ParseException parseException) {
					// ignore
				}
			}
		}
		
		// 
		if(tryDateHour) {
			
			//
			for(final String pattern : DATE_HOUR_FORMATTERS.getPatterns()) {
				
				try {
					return parseToDate(value, pattern);
					
				}catch(final ParseException parseException) {
					// ignore
				}
			}
		}
		
		try {
			return FORMAT.parse(value);
			
		}catch(final ParseException parseException) {
			// ignore
		}
		
		throw new ParseException(I18N.time().unknownFormatDateTime(value), 0);
	}
	
	/**
	 * @param value
	 * @return the {@link Date}
	 * @throws ParseException
	 */
	public static Date parse(final String value) throws ParseException {
		
		return parse(value, true, true, true);
	}
	
	/**
	 * @param value
	 * @param format
	 * @return formatted date value
	 * @throws ParseException
	 */
	public static String parse(final String value, final String format) throws ParseException {
	
		ParameterCheck.isNullOrAbsoluteEmpty(value, "value");
		ParameterCheck.isNullOrAbsoluteEmpty(format, "format");
		
		return parse(parse(value), format);
	}
	
	/**
	 * @param date
	 * @param indexParser
	 * @return formatted date
	 * @see #parseDate(long, int)
	 */
	public static String parseDate(final Date date, final int indexParser) {
	
		ParameterCheck.isNull(date, "date");
		
		return parseDate(date.getTime(), indexParser);
	}
	
	/**
	 * @param indexParser
	 * @return formatted date
	 * @see #parseDate(Date, int)
	 */
	public static String parseDate(final int indexParser) {
	
		return parseDate(new Date(), indexParser);
	}
	
	/**
	 * @param timestamp
	 * @param indexParser
	 * @return formatted date
	 */
	public static String parseDate(final long timestamp, final int indexParser) {
	
		return new SimpleDateFormat(getParserDate(indexParser)).format(new Date(timestamp));
	}
	
	/**
	 * @param date
	 * @param indexParser
	 * @return formatted date
	 * @see #parseDateHour(long, int)
	 */
	public static String parseDateHour(final Date date, final int indexParser) {
	
		ParameterCheck.isNull(date, "date");
		
		return parseDateHour(date.getTime(), indexParser);
	}
	
	/**
	 * @param timestamp
	 * @param indexParser
	 * @return formatted date
	 */
	public static String parseDateHour(final long timestamp, final int indexParser) {
	
		return new SimpleDateFormat(getParserDateHour(indexParser)).format(new Date(timestamp));
	}
	
	/**
	 * @param date
	 * @param indexParser
	 * @return formatted date
	 * @see MyDateUtil.parseHour()
	 */
	public static String parseHour(final Date date, final int indexParser) {
	
		ParameterCheck.isNull(date, "date");
		
		return parseHour(date.getTime(), indexParser);
	}
	
	/**
	 * @param timestamp
	 * @param indexParser
	 * @return formatted date
	 */
	public static String parseHour(final long timestamp, final int indexParser) {
	
		return new SimpleDateFormat(getParserHour(indexParser)).format(new Date(timestamp));
	}
	
	/**
	 * @param value
	 * @return the {@link Time}
	 * @throws ParseException
	 */
	@SuppressWarnings("deprecation")
	public static Time parseTime(final String value) throws ParseException {
		
		//
		for(final String pattern : HOUR_FORMATTERS.getPatterns()) {
			
			try {
				//
				Date date = new SimpleDateFormat(pattern).parse(value);
				
				return new Time(date.getHours(), date.getMinutes(), date.getSeconds());
				
			}catch(final ParseException parseException) {
				// ignore
			}
		}
		
		throw new ParseException(I18N.time().unknownFormatTime(value), 0);
	}
	
	/**
	 * @param value
	 * @return the {@link Timestamp}
	 * @throws ParseException
	 */
	public static Timestamp parseTimestamp(final String value) throws ParseException {
		
		//
		for(final String pattern : HOUR_FORMATTERS.getPatterns()) {
			
			try {
				//
				Date date = new SimpleDateFormat(pattern).parse(value);
				
				return new Timestamp(date.getTime());
				
			}catch(final ParseException parseException) {
				// ignore
			}
		}
		
		throw new ParseException(I18N.time().unknownFormatTime(value), 0);
	}
	
	/**
	 * @param date
	 * @return date in format <b>yyyy-MM-dd'T'HH:mm:ssXXX</b>
	 */
	public static String parseToUTC(final Date date) {
	
		ParameterCheck.isNull(date, "date");
		
		return UTC_FORMAT.format(date);
	}
	
	/**
	 * @param gregorianCalendar
	 * @param locale
	 * @return string representation
	 */
	public static String toString(final GregorianCalendar gregorianCalendar, final Locale locale) {
	
		ParameterCheck.isNull(gregorianCalendar, "gregorianCalendar");
		ParameterCheck.isNull(locale, "locale");
		
		// TODO: Melhorar para Idiomas cujo os dias da semana (ShortWeekdays) possuam a quantidade de caracteres != 3
		
		final String[] weekdays = new DateFormatSymbols(locale).getShortWeekdays();
		
		final GregorianCalendar now = new GregorianCalendar(gregorianCalendar.get(Calendar.YEAR), gregorianCalendar.get(Calendar.MONTH), gregorianCalendar.get(Calendar.DAY_OF_MONTH));
		
		final StringBuilder strCalendar = new StringBuilder("===========================================");
		
		strCalendar.append(SystemProperties.getLineSeparator());
		
		final StringBuilder titulo = new StringBuilder(now.getDisplayName(Calendar.MONTH, Calendar.LONG, locale) + " - " + now.get(Calendar.YEAR));
		
		final StringBuilder tituloAux = new StringBuilder("|");
		
		final int v = (43 - titulo.length()) / 2;
		
		//
		for(int c = 0; c < v; c++) {
			
			tituloAux.append(" ");
		}
		
		tituloAux.append(titulo);
		
		//
		while(tituloAux.length() < 42) {
			
			tituloAux.append(" ");
		}
		
		tituloAux.append("|");
		
		strCalendar.append(tituloAux);
		
		strCalendar.append(SystemProperties.getLineSeparator() + "===========================================");
		
		final StringBuilder strDaysOfWeek = new StringBuilder();
		
		//
		for(int index = 1; index < weekdays.length; index++) {
			
			strDaysOfWeek.append("| " + weekdays[index] + " ");
		}
		
		strDaysOfWeek.append("|");
		
		strCalendar.append(SystemProperties.getLineSeparator() + strDaysOfWeek.toString());
		
		final StringBuilder strLineSeparator = new StringBuilder();
		
		//
		for(int index = 0; index < strDaysOfWeek.length(); index++) {
			
			strLineSeparator.append(strDaysOfWeek.charAt(index) == '|' ? '|' : '=');
		}
		
		strCalendar.append(SystemProperties.getLineSeparator() + strLineSeparator.toString());
		
		final GregorianCalendar calendar = new GregorianCalendar(now.get(Calendar.YEAR), now.get(Calendar.MONTH), 1);
		
		String first = "| " + calendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, locale) + " |";
		
		final StringBuilder strDays = new StringBuilder();
		
		// Enquanto estiver dentro do mês
		while(calendar.get(Calendar.MONTH) == now.get(Calendar.MONTH)) {
			
			//
			for(int index = 1; index < weekdays.length; index++) {
				
				//
				if(first.equals("| " + weekdays[index] + " |")) {
					
					//
					if(calendar.get(Calendar.MONTH) != now.get(Calendar.MONTH)) {
						
						strDays.append("|     ");
						
						continue;
					}
					
					strDays.append("| ");
					
					//
					if(calendar.get(Calendar.DAY_OF_MONTH) < 10) {
						
						strDays.append(" ");
					}
					
					strDays.append(calendar.get(Calendar.DAY_OF_MONTH) + "  ");
					
					calendar.add(Calendar.DAY_OF_MONTH, 1);
					
					first = "| " + calendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, locale) + " |";
					
				}else {
					
					strDays.append("|     ");
				}
				
				if(index == 7) {
					
					strDays.append("|" + SystemProperties.getLineSeparator());
				}
			}
		}
		
		strCalendar.append(SystemProperties.getLineSeparator() + strDays.toString());
		
		//
		if( !strDays.toString().endsWith("" + SystemProperties.getLineSeparator())) {
			
			strCalendar.append(SystemProperties.getLineSeparator());
		}
		
		strCalendar.append("===========================================");
		
		return strCalendar.toString();
	}
}
